歡迎來到第七章!本章總共有兩篇內容:
這些主題的核心功能,並非由 Django Ninja 實作,但框架仍提供了一定程度的整合。並且,這些功能對於任何 Django 專案來說,都至關重要。
本文介紹幾乎所有 API 專案都需要的——身分認證(Authentication)。
我們將探討如何在 Django Ninja 中利用 Django 內建的 session-based 認證,實現完整的登入驗證功能,並進一步說明如何設定全域認證,以減少程式碼的重複。
本文所有的程式碼改動,可參考這個 PR。
進入實作前,我們要先了解,所謂的身分認證,究竟代表什麼。
以「帳號密碼 + session 認證」為例,身分認證的範圍主要涵蓋兩個階段。
首先,當使用者透過帳號密碼進行登入時,系統會檢查這些內容、確認身分合法。登入成功後,系統會將使用者資訊(比如用戶 id)儲存至 session,以維持登入狀態。
這是登入時的認證,也是我們最常說的認證。(狹義的認證)
接著,當使用者嘗試存取受「認證保護」的 API 時,系統會檢查 session 並確認身分,確保每個 API 請求都來自合法登入的使用者。
簡言之:
兩個層次相輔相成、一體兩面,確保服務能夠在使用者登入和後續操作中,提供適當的安全保障。
了解了上述兩個層次後,我們要先來實作「狹義」的認證——也就是登入驗證本身。
我們將建立一個「使用者登入」API,並直接透過 Django 的authenticate和login函式處理帳號密碼驗證和登入狀態——非常方便!
authenticate用來驗證使用者輸入的帳號(username)和密碼是否正確,login則將使用者的登入狀態儲存至 session。
先新增一個登入請求 Schema:
# user/schemas.py
class LoginRequest(Schema):
username: str = Field(examples=['Alice'])
password: str = Field(examples=['password123'])
然後是 view 函式:
from django.contrib.auth import authenticate, login
from user.schemas import CreateUserRequest, LoginRequest
...
@router.post('/users/login/', summary='登入使用者')
def login_user(
request: HttpRequest, payload: LoginRequest
) -> dict[str, str]:
"""
登入使用者
"""
user = authenticate(
request,
username=payload.username,
password=payload.password
)
if user is not None:
login(request, user) # 將使用者登入狀態儲存至 session
return {'message': '登入成功'}
else:
raise HttpError(401, '帳號或密碼錯誤')
非常簡單!
附帶一提,我不太喜歡程式中有「不必要」的else,此時的寫法仍不盡理想——因為else完全可以省略。
在最新的程式碼中,你可以看到我已改成:
user = authenticate(...)
if user is None:
raise HttpError(401, '帳號或密碼錯誤')
login(request, user) # 將使用者登入狀態儲存至 session
return {'message': '登入成功'}
這樣的做法即所謂的 Guard Clause 或 Early Return(雖然這裡是 raise)。
authenticate和login的用法幾乎是固定的,很容易理解:
authenticate在驗證成功時會 return 對應的User物件,失敗時則返回None。login不會 return,但request和user為必要的參數。成功登入後,你會得到 200 回應,並獲得兩組 cookie:

這對於 API client(比如 Postman)使用者很重要,畢竟瀏覽器會自動幫你存,但這些工具可不會——好吧,我錯了,至少我用的 RapidAPI 會自動存儲、發送!
(我測試 API 時還覺得奇怪,怎麼認證防護都失效了🤣)
如果工具沒有幫你做,記得自己在請求的 headers 加上:
POST /users/2/avatar/ HTTP/1.1
...
Cookie: csrftoken=...; sessionid=...
X-CSRFToken: ...
authenticate預設是以AbstractUser的username欄位和密碼作為認證基準,如果想用別的欄位,比如email,則要自己覆寫 Django 的認證後端。
登入功能完成後,接下來要將「需要登入才能存取」的 API,分別加上認證保護,使用 Django Ninja 提供的django_auth——這是專門給 Django 內建的 session 認證使用。
我們以「上傳 avatar」API 為例:
from ninja.security import django_auth
...
@router.post(
path='/users/{int:user_id}/avatar/',
summary='上傳 avatar',
auth=django_auth # 加上這組參數
)
這個例子中,auth=django_auth確保只有「已登入的使用者」才能存取此 API,否則將得到 401 或 403 回應。
但你可能會想到:
光是驗證「已登入」還不夠吧?
「上傳 avatar」應該只能幫「自己」上傳,總不能幫「別人」上傳大頭照吧!
沒錯,所以我們在 view 函式內部,還要多一層驗證。
request.user傳統的 Django 專案,我們會透過函式的第一參數——request,用request.user來獲得當前使用者資訊,比如:(參考文件)
if request.user.is_authenticated:
# Do something for authenticated users.
...
else:
# Do something for anonymous users.
...
具體來說:
request.user會是一個User實例,代表當前登入的使用者。request.user則是一個AnonymousUser實例,代表未登入使用者。當使用者已登入,我們可以檢查request.user的屬性,比如request.user.id,來確認是否為「本人」。
request.auth但寫 Django Ninja 則需要使用它提供的request.auth,實作結果如下:
...
def upload_avatar(...) -> dict[str, str]:
"""
上傳 avatar
"""
# 檢查登入的使用者是否為「本人」
if request.auth.id != user_id:
raise HttpError(403, '無權限上傳其他使用者的 avatar')
...
測試一下,登入後在 URL path 打別人的 id 來呼叫此 API:
// 403 Forbidden
{
"detail": "無權限上傳其他使用者的 avatar"
}
非常好!
雖然這裡用request.auth來取代request.user,但其實兩者的內涵有很大的不同。
在 Django Ninja 中,request.auth代表的是認證流程 return 的結果。此外,Django Ninja 允許你自定義認證方法,所以request.auth的內容是不固定的。
讓我們深入了解一下。
request.auth包含了當前認證方法返回的值。
User物件、字串、Python 字典等等。request.auth是 Django 的User物件。request.auth可能是 API key 本身或與之相關的資訊。request.auth可能包含解碼後的 token 資訊。總之,只要記得,想在 view 函式內進一步取得認證資訊,要透過request.auth。
這樣就已經實作完認證了,但我們可以讓事情更「簡單」一點。
一一對每個 API 設定認證保護,感覺有點繁瑣——尤其在 API 多的時候。
對此,Django Ninja 支援全域認證,讓所有 API 預設都直接受到保護,開發者只需在特定路由中進行例外處理,排除不想套用的 API 即可。
實作上非常簡單,Django Ninja 直接提供了SessionAuth認證類別,用來處理全域的 session-based 認證。
SessionAuth在專案的api.py中加入下面內容:
# NinjaForum/api.py
from ninja.security import SessionAuth
...
api = NinjaAPI(
auth=SessionAuth(), # 設定全域認證
...
)
如此一來,全部的 API 都預設擁有認證保護,你可以在特定 API 中排除,比如「登入使用者」:
@router.post(path='/users/login/', summary='登入使用者', auth=None)
在路由裝飾器中,把auth定義為None,解除認證保護。
我們來測試一下「有認證保護」的 API,你會發現在未登入的情況下,嘗試不同 HTTP 方法的 API,你將會得到不同的錯誤回應:
所以前面才會說你會得到「401 或 403」回應。
在我們的專案設計中,只有登入的使用者才能存取「取得所有使用者」API。
未登入的情況下,你會得到 401 回應:
// 401 Unauthorized
{
"detail": "Unauthorized"
}
未登入也無法存取「新增文章」API——這顯然非常合理,否則文章不就沒作者了😅
你會得到 403 回應:
// 403 Forbidden
{
"detail": "CSRF check Failed"
}
你心想:「奇怪?為什麼是 CSRF check Failed?」
這是 Django 的 CSRF 保護機制,因為我們的 API 是 POST 方法,所以 Django 會自動檢查 CSRF token,但我們沒有提供 CSRF token,所以就會出現這個錯誤。
在這篇文章中,我們探討了 Django 的 session 認證與 Django Ninja 的整合,實作了「使用者登入」API,並為其他 API 加上認證保護。最後還示範了如何實現全域認證,讓整個流程更加簡單。
這個系列的最後實踐,我們要來為專案——寫測試!
下一篇將探討,如何使用 test client 和 pytest 來為我們的 Django API 撰寫單元測試。這不僅能幫助我們驗證現有功能,還能為未來的開發和重構提供多一層的保障。
本文同步發表於我的部落格——Code and Me